Feat: searchable project selector#3068
Conversation
The project selector previously loaded up to 100 projects upfront, silently truncating orgs with more. Replace InputSelect with Input.ComboBox per row — each row fetches the first 25 projects on open, then debounces API search as the user types. Already-selected projects are filtered from each row's results. Removes the orgProjects prefetch from createMember entirely; edit modal no longer needs a projects prop.
Two root causes of the shift were identified and fixed:
1. Melt UI's `usePopper` applies `position: absolute` to the dropdown
via floating-ui after a `tick()` delay. During that one frame the
`ul` was briefly in normal document flow, momentarily growing the
`<dialog>` element and causing a reflow. Pre-setting
`[data-melt-combobox-menu] { position: absolute }` in base.css
removes the element from flow immediately at mount.
2. Melt UI's ComboBox has `preventScroll: true` by default. On open it
calls `removeScroll()` which sets `body.overflow: hidden` and adds
compensatory `padding-right` for the scrollbar width — a body
reflow that shifts the dialog. Pre-setting `data-melt-scroll-lock`
on the body in `modal.svelte` tells Melt UI the lock is already
active so it returns early without touching the body.
Also fix `on:search` on `Input.ComboBox` (which the component never
dispatches) by replacing it with native `oninput`/`onfocusin` handlers
on the wrapper `<div>`, so typed text actually triggers debounced
server-side project search. Projects are now ordered newest-first
(`Query.orderDesc('')`) matching the org projects page.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Greptile SummaryThis PR replaces the static project list prop on
Confidence Score: 4/5Safe to merge with minor follow-up; the async search logic is well-guarded but a few rough edges remain in the combobox UX. The generation-counter race fix and sibling-cache invalidation are solid improvements. The main outstanding concerns are the removed Add project disabled guard (users can add rows that can never be filled), open threads around empty-search feedback and the modal scroll-lock conflict with nested modals, and a mixed Svelte 4/5 event-syntax inconsistency. None of these are data-loss issues, but the UX gaps and the open threads from the previous review round suggest the component deserves another pass before merging. src/routes/(console)/organization-[organization]/projectAccessSelector.svelte and src/lib/components/modal.svelte warrant the closest look. Important Files Changed
Reviews (3): Last reviewed commit: "perf: prefetch project list on component..." | Re-trigger Greptile |
When a project was selected in one row, other rows' cached option lists still included it, allowing duplicate project assignments. Two places needed cache busting: - `onProjectSelected(i)`: clears options for all rows except the one that just made a selection; they reload fresh (with takenIds applied) on next focus. - `removeRow(i)`: clears all sibling caches after removal so the freed-up project reappears in other rows' dropdowns on next open. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Two bugs in the project access selector: 1. Race condition: loadProjects had no cancellation guard. Two in-flight requests for the same row (e.g. rapid typing) could resolve out of order, leaving stale results visible. Added a per-row generation counter — responses are discarded if a newer request has been dispatched since they were started. 2. Edit-mode UUIDs: when the edit modal opens with a member's existing project-specific roles, rowOptions starts empty so Input.ComboBox falls back to displaying the raw projectId. A $effect now eagerly calls loadProjects for any row that has a projectId but no loaded options, resolving the label as soon as the dropdown mounts.
…delay The project list was only fetched after a row appeared, causing a ~1 second blank dropdown. Now the API call fires the instant the ProjectAccessSelector mounts (when the user switches to "Specific projects"), so data is ready or in-flight before any interaction. All rows share a single Promise for unfiltered loads via prefetchPromise; typed searches still hit the API individually. The generation-counter race guard still applies so concurrent awaits can't overwrite each other.
What does this PR do?
(Provide a description of what this PR does.)
Test Plan
(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)
Related PRs and Issues
(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)
Have you read the Contributing Guidelines on issues?
(Write your answer here.)